use std::{cmp, ffi::c_int, mem, time::Duration};

use libc::{c_void, mode_t, size_t, S_IFMT, S_IFSOCK};

use super::{error::UnixError, ipc_bridge::OsOpaqueIpcChannel, ipc_sender::OsIpcSender, shared_memory::OsIpcSharedMemory, utils::{MsgControlLen, RECVMSG_FLAGS}};

use std::ffi::CString;
use super::utils;



#[derive(Copy, Clone)]
pub(super) enum BlockingMode {
    Blocking,
    Nonblocking,
    Timeout(Duration),
}

#[allow(clippy::uninit_vec, clippy::type_complexity)]
pub(super) fn recv(
    fd: c_int,
    blocking_mode: BlockingMode,
) -> Result<(Vec<u8>, Vec<OsOpaqueIpcChannel>, Vec<OsIpcSharedMemory>), UnixError> {
    let (mut channels, mut shared_memory_regions) = (Vec::new(), Vec::new());

    // First fragments begins with a header recording the total data length.
    //
    // We use this to determine whether we already got the entire message,
    // or need to receive additional fragments -- and if so, how much.
    let mut total_size = 0usize;
    let mut main_data_buffer;
    unsafe {
        // Allocate a buffer without initialising the memory.
        main_data_buffer = Vec::with_capacity(OsIpcSender::get_max_fragment_size());
        main_data_buffer.set_len(OsIpcSender::get_max_fragment_size());

        let mut iovec = [
            libc::iovec {
                iov_base: &mut total_size as *mut _ as *mut c_void,
                iov_len: mem::size_of_val(&total_size),
            },
            libc::iovec {
                iov_base: main_data_buffer.as_mut_ptr() as *mut c_void,
                iov_len: main_data_buffer.len(),
            },
        ];
        let mut cmsg = UnixCmsg::new(&mut iovec)?;

        let bytes_read = cmsg.recv(fd, blocking_mode)?;
        main_data_buffer.set_len(bytes_read - mem::size_of_val(&total_size));

        let cmsg_fds = CMSG_DATA(cmsg.cmsg_buffer) as *const c_int;
        let cmsg_length = cmsg.msghdr.msg_controllen;
        let channel_length = if cmsg_length == 0 {
            0
        } else {
            (cmsg.cmsg_len() - CMSG_ALIGN(mem::size_of::<cmsghdr>())) / mem::size_of::<c_int>()
        };
        for index in 0..channel_length {
            let fd = *cmsg_fds.add(index);
            if is_socket(fd) {
                channels.push(OsOpaqueIpcChannel::from_fd(fd));
                continue;
            }
            shared_memory_regions.push(OsIpcSharedMemory::from_fd(fd));
        }
    }

    if total_size == main_data_buffer.len() {
        // Fast path: no fragments.
        return Ok((main_data_buffer, channels, shared_memory_regions));
    }

    // Reassemble fragments.
    //
    // The initial fragment carries the receive end of a dedicated channel
    // through which all the remaining fragments will be coming in.
    let dedicated_rx = channels.pop().unwrap().to_receiver();

    // Extend the buffer to hold the entire message, without initialising the memory.
    let len = main_data_buffer.len();
    main_data_buffer.reserve_exact(total_size - len);

    // Receive followup fragments directly into the main buffer.
    while main_data_buffer.len() < total_size {
        let write_pos = main_data_buffer.len();
        let end_pos = cmp::min(
            write_pos + OsIpcSender::fragment_size(*utils::SYSTEM_SENDBUF_SIZE),
            total_size,
        );
        let result = unsafe {
            assert!(end_pos <= main_data_buffer.capacity());
            main_data_buffer.set_len(end_pos);

            // Integer underflow could make the following code unsound...
            assert!(end_pos >= write_pos);

            // Note: we always use blocking mode for followup fragments,
            // to make sure that once we start receiving a multi-fragment message,
            // we don't abort in the middle of it...
            let result = libc::recv(
                dedicated_rx.fd.get(),
                main_data_buffer[write_pos..].as_mut_ptr() as *mut c_void,
                end_pos - write_pos,
                0,
            );
            main_data_buffer.set_len(write_pos + cmp::max(result, 0) as usize);
            result
        };

        match result.cmp(&0) {
            cmp::Ordering::Greater => continue,
            cmp::Ordering::Equal => return Err(UnixError::ChannelClosed),
            cmp::Ordering::Less => return Err(UnixError::last()),
        }
    }

    Ok((main_data_buffer, channels, shared_memory_regions))
}

// https://github.com/servo/ipc-channel/issues/192
pub(super) fn new_msghdr(iovec: &mut [libc::iovec], cmsg_buffer: *mut cmsghdr, cmsg_space: MsgControlLen) -> libc::msghdr {
    let mut msghdr: libc::msghdr = unsafe { mem::zeroed() };
    msghdr.msg_name = std::ptr::null_mut();
    msghdr.msg_namelen = 0;
    msghdr.msg_iov = iovec.as_mut_ptr();
    msghdr.msg_iovlen = iovec.len() as super::utils::IovLen;
    msghdr.msg_control = cmsg_buffer as *mut c_void;
    msghdr.msg_controllen = cmsg_space;
    msghdr.msg_flags = 0;
    msghdr
}

#[cfg(not(all(target_os = "linux", feature = "memfd")))]
pub(super) fn create_shmem(name: CString, length: usize) -> c_int {

    unsafe {
        // NB: the FreeBSD man page for shm_unlink states that it requires
        // write permissions, but testing shows that read-write is required.
        let fd = libc::shm_open(
            name.as_ptr(),
            libc::O_CREAT | libc::O_RDWR | libc::O_EXCL,
            0o600,
        );
        assert!(fd >= 0);
        assert!(libc::shm_unlink(name.as_ptr()) == 0);
        assert!(libc::ftruncate(fd, length as libc::off_t) == 0);
        fd
    }
}

#[cfg(all(feature = "memfd", target_os = "linux"))]
fn create_shmem(name: CString, length: usize) -> c_int {
    unsafe {
        let fd = memfd_create(name.as_ptr(), libc::MFD_CLOEXEC as usize);
        assert!(fd >= 0);
        assert!(libc::ftruncate(fd, length as off_t) == 0);
        fd
    }
}

pub(super) struct UnixCmsg {
    cmsg_buffer: *mut cmsghdr,
    msghdr: libc::msghdr,
}

unsafe impl Send for UnixCmsg {}

impl Drop for UnixCmsg {
    fn drop(&mut self) {
        unsafe {
            libc::free(self.cmsg_buffer as *mut c_void);
        }
    }
}

impl UnixCmsg {
    unsafe fn new(iovec: &mut [libc::iovec]) -> Result<UnixCmsg, UnixError> {
        let cmsg_length = CMSG_SPACE(super::utils::MAX_FDS_IN_CMSG as usize * mem::size_of::<c_int>());
        let cmsg_buffer = libc::malloc(cmsg_length) as *mut cmsghdr;
        if cmsg_buffer.is_null() {
            return Err(UnixError::last());
        }
        Ok(UnixCmsg {
            cmsg_buffer,
            msghdr: new_msghdr(iovec, cmsg_buffer, cmsg_length as MsgControlLen),
        })
    }

    unsafe fn recv(&mut self, fd: c_int, blocking_mode: BlockingMode) -> Result<usize, UnixError> {
        match blocking_mode {
            BlockingMode::Nonblocking => {
                if libc::fcntl(fd, libc::F_SETFL, libc::O_NONBLOCK) < 0 {
                    return Err(UnixError::last());
                }
            },
            BlockingMode::Timeout(duration) => {
                let events = libc::POLLIN | libc::POLLPRI | libc::POLLRDHUP;
                let mut fd = [libc::pollfd {
                    fd,
                    events,
                    revents: 0,
                }];
                let result = libc::poll(
                    fd.as_mut_ptr(),
                    fd.len() as libc::c_ulong,
                    duration.as_millis().try_into().unwrap_or(-1),
                );

                match result.cmp(&0) {
                    cmp::Ordering::Equal => return Err(UnixError::Errno(libc::EAGAIN)),
                    cmp::Ordering::Less => return Err(UnixError::last()),
                    cmp::Ordering::Greater => {},
                }
            },
            BlockingMode::Blocking => {},
        }

        let result = libc::recvmsg(fd, &mut self.msghdr, RECVMSG_FLAGS);

        let result = match result.cmp(&0) {
            cmp::Ordering::Equal => Err(UnixError::ChannelClosed),
            cmp::Ordering::Less => Err(UnixError::last()),
            cmp::Ordering::Greater => Ok(result as usize),
        };

        if let BlockingMode::Nonblocking = blocking_mode {
            if libc::fcntl(fd, libc::F_SETFL, 0) < 0 {
                return Err(UnixError::last());
            }
        }
        result
    }

    unsafe fn cmsg_len(&self) -> size_t {
        (*(self.msghdr.msg_control as *const cmsghdr)).cmsg_len as size_t
    }
}

fn is_socket(fd: c_int) -> bool {
    unsafe {
        let mut st = mem::MaybeUninit::uninit();
        if libc::fstat(fd, st.as_mut_ptr()) != 0 {
            return false;
        }
        S_ISSOCK(st.assume_init().st_mode as mode_t)
    }
}

// FFI stuff follows:

#[cfg(all(feature = "memfd", target_os = "linux"))]
unsafe fn memfd_create(name: *const c_char, flags: usize) -> c_int {
    syscall!(MEMFD_CREATE, name, flags) as c_int
}

#[allow(non_snake_case)]
pub(super) fn CMSG_LEN(length: size_t) -> size_t {
    CMSG_ALIGN(mem::size_of::<cmsghdr>()) + length
}

#[allow(non_snake_case)]
pub(super) unsafe fn CMSG_DATA(cmsg: *mut cmsghdr) -> *mut c_void {
    (cmsg as *mut libc::c_uchar).add(CMSG_ALIGN(mem::size_of::<cmsghdr>())) as *mut c_void
}

#[allow(non_snake_case)]
pub(super) fn CMSG_ALIGN(length: size_t) -> size_t {
    (length + mem::size_of::<size_t>() - 1) & !(mem::size_of::<size_t>() - 1)
}

#[allow(non_snake_case)]
pub(super) fn CMSG_SPACE(length: size_t) -> size_t {
    CMSG_ALIGN(length) + CMSG_ALIGN(mem::size_of::<cmsghdr>())
}

#[allow(non_snake_case)]
fn S_ISSOCK(mode: mode_t) -> bool {
    (mode & S_IFMT) == S_IFSOCK
}

#[repr(C)]
pub(super) struct cmsghdr {
    pub(super) cmsg_len: MsgControlLen,
    pub(super) cmsg_level: c_int,
    pub(super) cmsg_type: c_int,
}

#[repr(C)]
pub(super) struct linger {
    pub(super) l_onoff: c_int,
    pub(super) l_linger: c_int,
}